Refine remembered method calls and property fetches when their receiver is narrowed#6008
Open
phpstan-bot wants to merge 1 commit into
Open
Refine remembered method calls and property fetches when their receiver is narrowed#6008phpstan-bot wants to merge 1 commit into
phpstan-bot wants to merge 1 commit into
Conversation
…er is narrowed - Add `MutatingScope::refreshDependentCallsAndFetches()`, called at the end of `filterBySpecifiedTypes()`, which re-derives every remembered `MethodCall`, `NullsafeMethodCall`, `PropertyFetch` and `NullsafePropertyFetch` whose receiver was just narrowed, and intersects the remembered (control-flow narrowed) type with the type derived from the now-narrower receiver. - This fixes the case where an earlier control-flow narrowing (e.g. removing `null` after `=== null`) recorded a call/fetch type against a broader receiver, and a later `instanceof` (or any condition narrowing) made the receiver more specific without refreshing the dependent call/fetch. - Refresh both PHPDoc and native expression types. - The refresh is driven by receiver narrowing (only from `filterBySpecifiedTypes`), so it never touches call/fetch types recorded by direct assignment to a property (which may legitimately hold a covariant subtype not compatible with the declared property type). - Covers the property-fetch analogue of the reported method-call bug.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
After a control-flow narrowing recorded a type for a method call or property
fetch (for example,
$x->getValue()narrowed toint|stringafter anif ($x->getValue() === null) { return; }), a laterinstanceofcheck thatmade the receiver (
$x) more specific did not update that remembered type.The call/fetch kept its stale, broader type instead of combining it with the
type derived from the now-narrower receiver.
This meant e.g.
assertType('string', $x->getValue())insideif ($x instanceof StringValue)reportedint|stringeven thoughStringValue::getValue()covariantly returnsstring.Changes
src/Analyser/MutatingScope.phprefreshDependentCallsAndFetches(array $narrowedExprStrings):scans the remembered expression types for
MethodCall,NullsafeMethodCall,PropertyFetchandNullsafePropertyFetchwhoseimmediate receiver (
->var) was just narrowed, re-derives their type fromthe current (narrower) receiver via the expression handlers, and intersects
it with the remembered type. Both PHPDoc and native expression types are
refreshed.
resolveExprHandlerType()helper that resolves an expression'stype through the
ExprHandlerregistry, bypassing the memoized entry.filterBySpecifiedTypes()now calls the refresh with the set of expressionsthat were narrowed by the condition.
tests/PHPStan/Analyser/nsrt/bug-10050.php— regression test with both amethod-call case (covariant
getValue()override across interfaces) and aproperty-fetch case (
A|Breceiver narrowed toA).Root cause
Remembered (control-flow narrowed) types for calls/fetches are stored against
the receiver type that was current at the time of narrowing. When the receiver
is later narrowed further, the remembered type is not a subtype of the newly
derivable type, and nothing recombined the two. The reported issue was one
instance (method calls); the same pattern affects property fetches, so both are
handled by keying the refresh on receiver narrowing.
Doing this by receiver narrowing — rather than by unconditionally recomputing
call/fetch types on every read — is important: property fetches can also be
memoized by direct assignment (
$this->foo = new Fooo()), where the storedtype may be a covariant subtype that is intentionally not compatible with the
declared property type. Those are not driven by receiver narrowing, so they are
left untouched (verified by
tests/PHPStan/Rules/Properties/data/bug-3777.php).Test
tests/PHPStan/Analyser/nsrt/bug-10050.php:testMethod()reproduces the reported bug:$v->get()isstringinsideif ($v instanceof StringValue)afternullwas removed earlier.testProperty()covers the analogous property-fetch case:$x->pisintinside
if ($x instanceof A)for anA|Breceiver.int|string) and pass after.make tests) and self-analysis (make phpstan) pass.Fixes phpstan/phpstan#10050